<template>
  <div class="upload-page">
    <h1>Upload</h1>

    <!-- 上传组件区域 -->
    <div class="upload-wrapper pv-10">
      <!-- naive-ui 的 n-upload 组件，用于实现文件/目录上传 -->
      <n-upload
        :show-file-list="false"
        :default-upload="false"
        :on-before-upload="checkFileType"
        :on-change="onChange"
        :directory="controlledDirectory"
        multiple
        directory-dnd
        accept=".jpg,.jpeg,.bmp,.webp,.gif,.png,.map4,.mp3,.pdf,.doc,.docx,.ppt,.pptx,.xls,.xlsx"
        ref="uploadRef"
      >
        <n-upload-dragger>
          <div style="margin-bottom: 12px">
            <n-icon size="48" :depth="3">
              <archive-icon/>
            </n-icon>
          </div>
          <n-text style="font-size: 16px">拖动目录或者多个文件到该区域来上传</n-text>
          <n-p depth="3" style="margin: 8px 0 0 0">
            支持的文件类型为：.jpg,.jpeg,.bmp,.webp,.gif,.png,.map4,.mp3,.pdf,.doc,.docx,.ppt,.pptx,.xls,.xlsx
          </n-p>
        </n-upload-dragger>
      </n-upload>
    </div>
    <div class="btns-wrapper mt-10">
      <n-button :type="controlledDirectory ? 'default' : 'primary'" @click="onSelectFile"
      >选择文件
      </n-button>
      <n-button
        :type="controlledDirectory ? 'primary' : 'default'"
        class="ml-10"
        @click="onSelectDirectory"
      >选择文件夹
      </n-button>
    </div>

    <!-- 数据表格展示已选择文件信息 -->
    <n-data-table :columns="tableColumns" :data="tableData" class="mt-10"/>

    <!-- 文件上传统计标签 -->
    <div class="tag-wrapper f mt-10">
      <n-tag> 文件数量:{{ tagGroupData.fileCount }}</n-tag>
      <n-tag type="success" class="mh-10"> 成功上传:{{ tagGroupData.successfulFileCount }}</n-tag>
      <n-tag> 上传总大小: {{ tagGroupData.uploadedSize + 'byte' }}</n-tag>
    </div>

    <!-- 控制按钮 -->
    <n-button class="mt-10 mr-10" :disabled="!tableData.length" @click="onManualUpload"
    >开始上传
    </n-button>
    <n-button class="mt-10" :disabled="!tableData.length" @click="onManualClear">
      清空列表
    </n-button>
  </div>
</template>

<script setup lang="ts">
import axios from 'axios'
import type { CancelTokenSource } from 'axios'
import type { UploadFileInfo } from 'naive-ui'
import { useApi } from '@/hooks/useApi'
import { NIcon, NUpload } from 'naive-ui'
import { ArchiveOutline as ArchiveIcon, Trash } from '@vicons/ionicons5'
import { useGlobalProperties } from '@/hooks/useGlobalProperties'

type FileStatus = 'uploading' | 'success' | 'error' | 'waiting'
type TableDataItem = {
  id: string
  name: string
  type: string
  size: number
  originalSize: number
  percentage: number
  status: FileStatus
  file: File
  cancelTokenSource?: CancelTokenSource | null
}
type TableData = TableDataItem[]
type SubList = { id: string; file: File; cancelTokenSource: CancelTokenSource }

// 定义允许上传的文件类型及其对应的 MIME 类型
const allowedFileTypesMap = {
  jpg: 'image/jpeg',
  jpeg: 'image/jpeg',
  bmp: 'image/bmp',
  webp: 'image/webp',
  gif: 'image/gif',
  png: 'image/png',
  map4: 'video/mp4',
  mp3: 'audio/mp3',
  pdf: 'application/pdf',
  doc: 'application/msword',
  docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
  ppt: 'application/vnd.ms-powerpoint',
  pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
  xls: 'application/vnd.ms-excel',
  xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
}
// 表格列配置
const tableColumns = [
  { title: '文件名', key: 'name' },
  { title: '类型', key: 'type' },
  { title: '大小', key: 'size', render: (row: TableDataItem) => h('span', null, row.size + 'M') },
  {
    title: '状态',
    key: 'status',
    render: (row: TableDataItem) =>
      row.percentage > 0
        ? h(NProgress, {
          percentage: row.percentage,
          indicatorPlacement: 'inside',
          processing: row.percentage < 100 && !!row.cancelTokenSource
        })
        : h('span', null, '')
  },
  {
    title: '操作',
    key: 'operate',
    render: ({ cancelTokenSource, percentage, id: fileId }, index) =>
      // 上传进度小于100%才显示取消上传按钮
      percentage < 100 &&
      h(
        'span',
        {
          // 按钮禁用点击样式，外层禁用样式，内层禁用点击
          class: `${!tableData.value[index].cancelTokenSource ? 'cursor-not-allowed' : 'cursor-pointer'}`
        },
        h(
          NIcon,
          {
            size: 20,
            class: `${!tableData.value[index].cancelTokenSource ? 'point-events-none' : 'cursor-pointer'}`,
            // 取消请求后图标变红
            color: !tableData.value[index].cancelTokenSource ? 'red' : '',
            title: '取消上传',
            onClick: () => {
              if (cancelTokenSource) {
                console.log(`Cancelling upload of file ${fileId}: User cancelled.`)
                cancelTokenSource.cancel('用户取消上传')
                tableData.value[index].cancelTokenSource = null
              }
            }
          },
          { default: () => h(Trash) }
        )
      )
  }
]

// 初始化全局状态
const limitCount = 6 // 每次限制上传文件最大数量
const { business: businessApi } = useApi()
const { $message, $to } = useGlobalProperties()

// 是否控制上传目录
const controlledDirectory = ref(false)
const uploadRef = ref<InstanceType<typeof NUpload> | null>(null)
// 表格数据
const tableData = ref<TableData>([])
// 文件上传相关数据
const tagGroupData = reactive({
  fileCount: 0, //文件数量
  successfulFileCount: 0, // 成功上传
  uploadedSize: 0 // 上传大小
})

// 定义辅助函数
// 上传文件前校验文件类型
const checkFileType: (options: {
  file: UploadFileInfo
  fileList: UploadFileInfo[]
}) => Promise<boolean | void> | boolean | void = ({ file }) => {
  return new Promise((resolve, reject) => {
    const { file: { type } = {} as any } = file || {}
    if (!Object.values(allowedFileTypesMap).includes(type)) {
      $message.error('文件类型不支持')
      reject(new Error('文件类型不支持'))
    } else {
      resolve(true)
    }
  })
}
// 文件大小计算
const getFileSize: (size: string, unit?: string) => number = (size, unit = 'MB') => {
  if (unit === 'MB') {
    return +((parseFloat(size) / 1024 / 1024) as number).toFixed(2)
  } else if (unit === 'byte') {
    return +(parseFloat(size) as number).toFixed(2)
  }
  return +(parseFloat(size) as number).toFixed(2)
}

// 定义事件处理函数
// 文件状态改变处理
const onChange: (options: {
  file: UploadFileInfo
  fileList: Array<UploadFileInfo>
  event?: Event
}) => void = ({ fileList }) => {
  tableData.value = fileList.map((fileItem, index) => {
    const { file: { name, size, type } = {} as any } = fileItem
    const computedSize = getFileSize(size) + 'MB'
    // 根据文件类型type获取对应的文件类型key
    const computedType =
      // eslint-disable-next-line @typescript-eslint/no-unused-vars
      Object.entries(allowedFileTypesMap).find(([_, value]) => value === type)?.[0] || ''
    // 取消请求source
    const cancelTokenSource = axios.CancelToken.source()
    return {
      id: `file-${index}`,
      name,
      type: computedType,
      originalSize: size,
      size: parseFloat(computedSize),
      percentage: 0,
      status: 'waiting',
      file: fileItem.file!,
      cancelTokenSource
    }
  })
  // 修改文件数量
  tagGroupData.fileCount = tableData.value.length
}
// 手动选择文件
const onSelectFile = () => {
  controlledDirectory.value = false
  nextTick(() => {
    uploadRef.value?.openOpenFileDialog()
  })
}
// 手动选择文件夹
const onSelectDirectory = () => {
  controlledDirectory.value = true
  nextTick(() => {
    uploadRef.value?.openOpenFileDialog()
  })
}
// 清空列表
const onManualClear = () => {
  tableData.value = []
  Object.assign(tagGroupData, { fileCount: 0, successfulFileCount: 0, uploadedSize: 0 })
  uploadRef.value?.clear()
}
// 手动上传
const onManualUpload = async () => {
  if (Array.isArray(tableData.value) && !tableData.value.length) return
  let subList: SubList[] = []
  const fileList: SubList[][] = []
  // 将文件列表按limitCount个为一组进行分组
  tableData.value.forEach((item, index) => {
    subList.push({
      id: item.id,
      file: item.file as File,
      cancelTokenSource: item.cancelTokenSource!
    })
    // index达到limitCount或者不足limitCount但是达到文件总个数时，将subList添加到fileList中，并清空subList
    if ((index + 1) % limitCount === 0 || index + 1 === tableData.value.length) {
      fileList.push(subList)
      subList = []
    }
  })

  for (let i = 0; i < fileList.length; i++) {
    const [error, resp] = await $to(limitCountFileUpload(limitCount, fileList[i], i))
    if (error) {
      $message.error('上传失败')
      return
    }
  }
}

// 分批上传文件的主函数
/**
 * 限制上传文件数量
 * @param count 每次限制上传文件最大数量
 * @param fileList {file: File; cancelTokenSource: CancelTokenSource}[]
 * @param reincarnation 轮询数(分片上传次数)
 */
const limitCountFileUpload = (count: number, fileList, reincarnation) => {
  return new Promise((resolve) => {
    const promises = []
    const len = fileList.length < count ? fileList.length : count
    for (let i = 0; i < len; i++) {
      const formData = new FormData()
      // 添加文件file对象
      formData.append('file', fileList[i].file)
      const params = {
        data: formData,
        // 文件上传进度更新方法
        onUploadProgress: (progressEvent: ProgressEvent) => {
          const { loaded, total } = progressEvent
          const percentage = Math.round((loaded / total) * 100)
          tableData.value[reincarnation * count + i].percentage = percentage
          //  更新上传大小
          tagGroupData.uploadedSize += getFileSize(
            (percentage / 100) * tableData.value[reincarnation * count + i].originalSize + '',
            'byte'
          )
        },
        cancelToken: fileList[i].cancelTokenSource.token
      }
      const promise = new Promise((resolve, reject) => {
        businessApi
          .uploadFile(params)
          .then(() => {
            tagGroupData.successfulFileCount++
            resolve(true)
          })
          .catch((err) => {
            reject(err)
          })
      })

      promises.push(promise)
    }
    Promise.allSettled(promises).then((resp) => {
      resolve(resp)
    })
  })
}
</script>

<style lang="scss">
.upload-page {
  position: absolute;
  inset: 0;
  padding: 10px;
}
</style>
